Skip to content

Conversation

@Hinton
Copy link
Member

@Hinton Hinton commented Oct 30, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-27575

📔 Objective

Adds support for overloading mail templates from disk.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@Hinton Hinton requested a review from a team as a code owner October 30, 2025 09:23
@Hinton Hinton requested a review from coroiu October 30, 2025 09:23
@claude
Copy link
Contributor

claude bot commented Oct 30, 2025

Claude finished @Hinton's task —— View job


Code Review Complete

Summary of Changes Since Last Review

Latest commit (9e608b4) contains only formatting changes (reordering using statements). No functional changes since previous review.

Addressing Previous Comment Thread

Responding to Hinton's question about Finding 1 from the previous review:

Previous Finding 1 is incorrect. The null reference scenario described cannot occur. Here's why:

Analysis

The BuildDirectory method in GlobalSettings.cs:143 only returns null when SelfHosted is false. When SelfHosted is true (which is checked on line 89), MailTemplateDirectory returns either:

  1. The explicit value if set, OR
  2. "/etc/bitwarden/mail-templates" as the default

Therefore, when the code reaches line 96, MailTemplateDirectory cannot be null. The check on line 89 ensures that if SelfHosted is false (when null could be returned), the method returns early.

Your response is correct - any exception would be caught by the catch block, but more importantly, the specific null reference scenario cannot actually occur with the current logic.


New Findings

Finding 1: Typo in test method name

Line 71 of HandlebarMailRendererTests.cs: Method name PrevenetsPathTraversal should be PreventsPathTraversal (missing 'v').

⚠️ Finding 2: Potential symbolic link bypass in path traversal protection
Details

The path traversal check (lines 100-105 in HandlebarMailRenderer.cs) validates the resolved path is within the base directory, but doesn't prevent symbolic link attacks. If a symlink exists inside the template directory pointing outside it, Path.GetFullPath resolves the symlink, but the validation occurs on the link target path.

Scenario:

  • Template directory: /etc/bitwarden/mail-templates
  • Attacker creates symlink: /etc/bitwarden/mail-templates/evil.hbs/etc/passwd
  • Request template: evil.hbs
  • diskPath resolves to /etc/passwd
  • Validation fails (correctly blocks this)

However, consider:

  • Symlink: /etc/bitwarden/mail-templates/subdir/etc
  • Request: subdir/passwd
  • diskPath resolves to /etc/passwd
  • Validation on resolved path would fail

The current implementation appears to handle this correctly by checking the resolved path. However, the risk exists if Path.GetFullPath doesn't fully resolve symlinks on all platforms.

Note: This requires filesystem write access to the template directory, which is already a significant security breach. Consider adding a check using File.ResolveLinkTarget (if available) or documenting that the template directory must have restricted write permissions.

🎨 Finding 3: Unnecessary variable assignment

Line 109 in HandlebarMailRenderer.cs can be simplified:

return await File.ReadAllTextAsync(diskPath);

Instead of:

var fileContents = await File.ReadAllTextAsync(diskPath);
return fileContents;
♻️ Finding 4: Missing file validation creates potential for resource exhaustion
Details

ReadSourceFromDiskAsync (lines 87-119) reads arbitrary file sizes without validation:

  • No file size limit check
  • No file extension validation
  • No content validation

This could allow:

  1. Reading extremely large files causing memory exhaustion
  2. Reading non-template files (though symlink protection helps)

Suggested additions:

var fileInfo = new FileInfo(diskPath);
if (fileInfo.Length > MAX_TEMPLATE_SIZE) // e.g., 1MB
{
    _logger.LogWarning("Template file too large: {Template}", template);
    return null;
}

Good Practices Observed

  • Comprehensive test coverage including security-focused path traversal tests
  • Thread-safe template caching with proper use of Lazy<T> and ConcurrentDictionary
  • Graceful fallback from disk to embedded resources

Action Items

  1. Fix typo in test method name (Finding 1)
  2. Consider file size validation to prevent resource exhaustion (Finding 4)
  3. Document template directory security requirements (restricted write permissions)
  4. Optional: Simplify variable assignment (Finding 3)

@@ -54,19 +67,50 @@ private async Task<HandlebarsTemplate<object, object>> CompileTemplateInternalAs
return handlebars.Compile(source);
}

private static async Task<string> ReadSourceAsync(Assembly assembly, string template)
Copy link
Member Author

@Hinton Hinton Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had to remove static from most functions because reading from disk relies on non static data.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 30, 2025

Logo
Checkmarx One – Scan Summary & Detailscdd9919b-3358-4e62-951d-ec914edb77ca

New Issues (4)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1173
detailsMethod at line 1173 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: mt90rvQDdoi8xKFa8Rh3w8pSm40%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1063
detailsMethod at line 1063 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: fmtC%2BryTi%2FuGlcLnwRiLrscpuKQ%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1537
detailsMethod at line 1537 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: DQQXKsIwd9pMh7QWJpaa4uxM1ak%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1408
detailsMethod at line 1408 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: uVQQ0WSSjrqK%2FgEoIPqs7yn0%2FhY%3D
Attack Vector
Fixed Issues (2)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs: 72
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 300

@codecov
Copy link

codecov bot commented Oct 30, 2025

Codecov Report

❌ Patch coverage is 87.87879% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.50%. Comparing base (087c691) to head (9e608b4).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...Core/Platform/Mail/Mailer/HandlebarMailRenderer.cs 87.87% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #6520       +/-   ##
===========================================
+ Coverage   13.30%   52.50%   +39.20%     
===========================================
  Files        1137     1910      +773     
  Lines       49787    84792    +35005     
  Branches     3879     7574     +3695     
===========================================
+ Hits         6625    44524    +37899     
+ Misses      43040    38542     -4498     
- Partials      122     1726     +1604     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coroiu
coroiu previously approved these changes Oct 30, 2025
# Conflicts:
#	test/Core.Test/Platform/Mailer/HandlebarMailRendererTests.cs
#	test/Core.Test/Platform/Mailer/MailerTest.cs
@Hinton Hinton requested a review from coroiu November 6, 2025 16:39
@Hinton Hinton enabled auto-merge (squash) November 6, 2025 16:39
coroiu
coroiu previously approved these changes Nov 7, 2025

try
{
var diskPath = Path.GetFullPath(Path.Combine(_globalSettings.MailTemplateDirectory, template));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding 1: Null reference exception when MailTemplateDirectory is null

Details

When SelfHosted is true but MailTemplateDirectory is not explicitly configured, GlobalSettings.BuildDirectory returns null (line 143 in GlobalSettings.cs). This causes Path.Combine and Path.GetFullPath to throw an ArgumentNullException.

The BuildDirectory method only returns a non-null value when:

  1. An explicit value is set, OR
  2. SelfHosted is true (returns /etc/bitwarden/mail-templates)

However, the check on line 89 only validates SelfHosted, not whether MailTemplateDirectory is null.

Suggested fix:

if (!_globalSettings.SelfHosted || string.IsNullOrWhiteSpace(_globalSettings.MailTemplateDirectory))
{
    return null;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's caught by the catch right?

var baseDirectory = Path.GetFullPath(_globalSettings.MailTemplateDirectory);

// Ensure the resolved path is within the configured directory
if (!diskPath.StartsWith(baseDirectory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Finding 2: Path traversal protection may fail on case-sensitive filesystems

The path validation uses StringComparison.OrdinalIgnoreCase, but this only affects string comparison logic, not actual filesystem behavior. On Linux (case-sensitive), an attacker could potentially bypass this check by exploiting case differences if symbolic links or actual directory names differ in case from the configured path.

Consider using Path.GetFullPath on both sides after resolving any symbolic links, or add explicit checks for symbolic link traversal.


if (File.Exists(diskPath))
{
var fileContents = await File.ReadAllTextAsync(diskPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Finding 3: Variable name fileContents can be simplified to content

The variable assignment on line 109 followed by immediate return on line 110 can be simplified to:

return await File.ReadAllTextAsync(diskPath);

This reduces unnecessary variable allocation and improves readability.

@Hinton Hinton merged commit 7d39efe into main Nov 10, 2025
43 of 45 checks passed
@Hinton Hinton deleted the arch/mailer-disk branch November 10, 2025 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants